package game.model;

import excepciones.JuegoGanadoException;
import excepciones.TrampaAlcanzadaException;

import java.awt.Point;
import java.util.HashSet;
import java.util.Set;

/**
 * Clase Tablero que contiene todo lo necesario para el manipulamiento
 * de Celdas y componentes sobre el.
 * Ademas lleva un registro de los origenes y destinos colocados en el, para
 * su futuro uso.
 * Tambien implementa los metodos para guardar los rayos en sus celdas, para verificar
 * si un juego jugado sobre el llego a su fin, y los metodos utilizados al llegar
 * un evento.
 *
 */

public class Tablero {
	private Celda[][] tablero;
	private Set<Point> origenes;
	private int destinos;

	/**
	 * Constructor de Tablero.
	 * 
	 * @param fil filas.
	 * @param col columnas.
	 */
	
	public Tablero(int fil, int col) {
		validaDimensiones(fil, col);
		this.tablero = new Celda[fil][col];
		this.origenes = new HashSet<Point>();
		this.destinos = 0;
	}

	/**
	 * Validador de dimension del tablero.
	 * 
	 * @param fil filas.
	 * @param col columnas.
	 * @throws IllegalArgumentException si los argumentos no fueran validos.
	 */
	
	private void validaDimensiones(int fil, int col) {
		if(fil < 5 || col < 5 || fil > 20 || col > 20) {
				throw new IllegalArgumentException();
		}
	}
	
	/**
	 * Retorna la celda guardada en la posicion dada.
	 * 
	 * @param posicion posicion en la que se quiere conocer la celda.
	 * @return Celda celda en dicha posicion.
	 */
	
	public Celda getCelda(Point posicion) {
		if(posicionValida(posicion)) {
			return tablero[posicion.x][posicion.y];
		} return null;
	}
	
	/**
	 * Guarda una celda ya armada en una determinada posicion.
	 * Completa los registros de origenes y destinos.
	 * 
	 * @param ubicacion posicion en el tablero.
	 * @param componente componente que guarda la celda.
	 * @throws IllegalArgumentException si los argumentos no fueran validos.
	 */
	
	public void setCelda(Point ubicacion, Componente componente) {
		if(!posicionValida(ubicacion)) {
			throw new IllegalArgumentException();
		}	
		if(componente instanceof Origen) {
			agregaOrigen(ubicacion);
		}
		if(componente instanceof Destino) {
			agregaDestino();
		}
		this.tablero[ubicacion.x][ubicacion.y] = new Celda(componente);
	}
	
	/**
	 * Agrega un origen al registro.
	 * 
	 * @param ubicacion ubicacion del origen a agregar.
	 */
	
	private void agregaOrigen(Point ubicacion) {
		this.origenes.add(ubicacion);
	}
	
	/**
	 * Agrega un destino al registro.
	 * 
	 * @param ubicacion ubicacion del destino a agregar.
	 */
	
	private void agregaDestino() {
		this.destinos++;
	}
	
	/**
	 * Retorna la cantidad de destinos, util a la hora de saber si el juego
	 * llego o no a su fin.
	 * 
	 * @return int cantidad de destinos.
	 */
	
	public int getDestinos() {
		return destinos;
	}
	
	/**
	 * Retorna los origenes.
	 * 
	 * @return Set<Point> set con las posiciones de los origenes.
	 */
	
	public Set<Point> getOrigenes(){
		return this.origenes;
	}
	
	/**
	 * Mueve una componente de origen a destino. Realiza un swap de componentes.
	 * 
	 * @param origen posicion origen.
	 * @param destino posicion destino.
	 */
	
	private void moverFicha(Point origen, Point destino){
		this.setCelda(destino, this.getCelda(origen).getComponente());
		this.setCelda(origen, new Vacio());
	}
	
	/**
	 * Retorna la componente guardada en una celda determinada.
	 * 
	 * @param posicion posicion de la celda.
	 * @return Componente componente contenida en la celda.
	 */
	
	public Componente getComponente(Point posicion){
		return this.getCelda(posicion).getComponente();
		                  
	}
	
	/**
	 * Setea la componente en una celda determinada.
	 * 
	 * @param point posicion de la celda.
	 * @param componente componente a agregar.
	 */
	
	public void setComponente(Point point, Componente componente){
		this.setComponente(point, componente);
	}
	
	/**
	 * Retorna true si la posicion pertenece al tablero.
	 * 
	 * @param posicion posicion en el tablero.
	 * @return true si el tablero inclute a dicha posicion.
	 */
	
	public boolean posicionValida(Point posicion) {
		boolean primero = posicion.x < this.tablero.length && posicion.y < this.tablero[0].length;
		boolean segundo = posicion.x >= 0 && posicion.y >= 0; 
		return primero && segundo;
	}
	
	/**
	 * Retorna el tamano del tablero.
	 * @return Point dimensiones del tablero.
	 */
	
	public Point getDimensiones() {
		return new Point(this.tablero.length, this.tablero[0].length);
	}
	
	/**
	 * Rota una componente.
	 * Este metodo es utilizado por el listener.
	 * 
	 * @param row fila de la componente a rotar.
	 * @param column columna de la componente a rotar.
	 */
	
	public void rotarPieza(int row, int column) {
		if(this.getCelda(new Point(row,column)).getComponente() instanceof Orientable) {
			((Orientable)this.getCelda(new Point(row,column)).getComponente()).rotar();
		}

	}

	/**
	 * Intercambia dos componentes.
	 * Este metodo es utilizado por el listener.
	 * 
	 * @param sourceRow fila de la componente a rotar.
	 * @param sourceColumn columna de la componente a rotar.
	 * @param targetRow fila de la otra componente a rotar.
	 * @param targetColumn columna de la otra componente a rotar.
	 */
	
	public void MoverPiezas(int sourceRow, int sourceColumn, int targetRow, int targetColumn) {
		if(this.getCelda(new Point(sourceRow, sourceColumn)).getComponente() instanceof Movible) {
			if(this.getCelda(new Point(targetRow, targetColumn)).getComponente() instanceof Vacio ) {
					this.moverFicha(new Point(sourceRow, sourceColumn), new Point(targetRow, targetColumn));
			}
		}
	}
	
	/**
	 * Retorna los puntos logrados en el juego.
	 * 
	 * @return int puntaje en el nivel.
	 */
	
	public int totalPuntos() {
		int suma = 0;
		for(int i = 0; i < this.getDimensiones().x; i++) {
			for(int j = 0; j < this.getDimensiones().y; j++) {
				if(this.getCelda(new Point(i,j)).sumaPuntos()) {
					suma++;
				}
			}
		}
		for(Point origen: origenes) {
			if(this.getCelda(origen).getRayos().size() == 0) {
				suma++;
			}
		}
		return suma;
	}
	
	/**
	 * Vacia el set de rayos de todas las celdas del tablero, para comenzar
	 * la proxima pasada con los registros limpios.
	 * 
	 */
	
	public void limpiaRayo(){
		for(int i = 0; i < this.getDimensiones().x; i++) {
			for(int j = 0; j < this.getDimensiones().y; j++) {
				this.getCelda(new Point(i,j)).getRayos().clear();
			}
		}
	}
	
	/**
	 * Verifica si el nivel llego a su fin, ya ser por tocar una trampa con
	 * un rayo o por haber logrado alcanzar los destinos con su propio color.
	 * En cualquier caso lanza la excepcion correspondiente.
	 * 
	 * @throws JuegoGanadoException si se alcanzan los destinos.
	 * @throws TrampaAlcanzadaException si se toca una trampa.
	 */
	
	public void verificaFin() {
		int destinos = 0;
		for(int i = 0; i < this.getDimensiones().x; i++) {
			for(int j = 0; j < this.getDimensiones().y; j++) {
				Celda actual = this.getCelda(new Point(i,j));
				if(actual.getComponente() instanceof Destino) {
					if(((Destino)actual.getComponente()).estaTocado(actual.getRayos())) {
						destinos++;
						if(getDestinos() == destinos) {
							throw new JuegoGanadoException();
						}
					}
				} else if(actual.getComponente() instanceof Trampa) {
					if(((Trampa)actual.getComponente()).finJuego(actual.getRayos())) {
						throw new TrampaAlcanzadaException();
					}
				}
			}
		}
	}

	/**
	 * Dibuja el tablero con rayos en el back end. Luego un metodo del front
	 * levantara esta informacion para pasarla a la pantalla.
	 * 
	 * @param posicion posicion de la celda actual a dibujar.
	 * @param entrada rayo con el que se entra a la celda en esta pasada.
	 */
	
	public void dibujar(Point posicion, Rayo entrada) {
		if(posicionValida(posicion)) {
			Set<Rayo> anterior = this.getCelda(posicion).getRayos();
			Set<Rayo> auxiliar = this.getCelda(posicion).getComponente().getSalida(entrada, anterior);
			for(Rayo siguiente: auxiliar) {
				Point actual = siguiente.getPoint(siguiente.getOrientacion());
				dibujar(new Point(posicion.x + actual.x, posicion.y + actual.y), new Rayo(siguiente.getColor(), siguiente.getOrientacion()));
			}
		}
	}
		
}
